package com.googlecode.YaccSyntaxTool.export;

import java.io.BufferedInputStream;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileWriter;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Calendar;
import java.util.Date;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Properties;
import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;

import com.googlecode.YaccSyntaxTool.grammar.FactorType;
import com.googlecode.YaccSyntaxTool.grammar.Syntax;


public class Exporter {

	/**
	 * Logger
	 */
	private final static Logger sLog = Logger.getLogger(Exporter.class.getName());

	private final static String		FORMAT_NONTERMINAL = "%1$-10s"; // add right padding

	private final static String		HTML_PARENT_DIR				= "../";
	private final static String		HTML_SUB_DIR_NONTERMINALS	= "nonterminals/";
	private final static String		HTML_SUB_DIR_SYNTAX			= "syntax/";
	private final static String		HTML_SUB_DIR_EXTRACTED		= "extracted/";

	private final static String		HTML_DIR_PREFIX_CHAPTER		= "chapter";

	private final static String		HTML_FILEENDING	= ".htm";

	private final static String		HTML_FILE_INDEX			= "index";
	private final static String		HTML_FILE_SYNTAX		= "syntax";
	private final static String		HTML_FILE_NONTERMINALS	= "nonterminals";
	private final static String		HTML_FILE_KEYWORDS		= "keywords";

	// special strings to alter behavior of formatStr
	public final static String		HTML_START_TAG		= "@#!#@START@#!#@";
	public final static String		HTML_END_TAG		= "@#!#@END@#!#@";


	private final static String		PROP_TITLE			= "export.title";

	private final static String		PROP_CHAPTER		= "export.chapter";
	private	 final static String		PROP_SUFFIX_TITLE	= ".title";

	private final static String		PROP_EXTRACT		= "export.extract";

	public enum ExportKind {
		TXT,
		HTML,
		HTMLCOMPACT
	}

	private StringBuffer	buf = null;
	private ExportKind		kind;

	private ReferenceMap	keywords;
	private ReferenceMap	nonterminals;

	// EXPORT properties (=configuration)
	private Properties		props;

	private List<Chapter>	content;
	private Set<String>		extracts; // <nonterminal>

	private final String	outputDirectory;
	private	 String			htmlRoot = "";

	public Exporter(String outputDirectory) {
		super();
		this.outputDirectory = outputDirectory;
	}

	public Exporter(Exporter parent) {
		this(parent.outputDirectory);
		// use same HTML root directory as parent exporter
		this.htmlRoot = parent.htmlRoot;
	}

	public ExportKind getKind() {
		return kind;
	}

	public void log(Syntax iSyntax) {

		if (iSyntax != null && sLog.isLoggable(Level.FINE)) {

			// initialize exporter
			buf = new StringBuffer();
			kind = ExportKind.TXT;

			// prepare text
			try {
				iSyntax.export(this);
			} catch (NotSupportedExportKindException e) {
				sLog.log(Level.SEVERE, e.getMessage(), e);
			}

			// log text
			sLog.log(Level.FINE, buf.toString());
		}
	}


	public void exportHtml(Syntax iSyntax) {
		if (iSyntax != null) {

			// load and handle properties
			loadProps();

			// initialize kind
			kind = ExportKind.HTML;

			// export table of content
			exportTableOfContent();

			// export complete syntax
			exportSyntax(iSyntax, HTML_SUB_DIR_SYNTAX, HTML_FILE_SYNTAX, true);

			// export specific chapters
			exportChapters(iSyntax);

			// export extracts
			exportExtracts(iSyntax);
		}
	}

	private void exportSyntax(Syntax iSyntax, String subDir, String filename, boolean exportReferenceMaps) {
		if (iSyntax != null) {

			// initialize html root directoy
			this.htmlRoot = subDir;

			// gather references (for this specific syntax)
			fillReferenceMaps(iSyntax);

			if (exportReferenceMaps) {
				exportNonterminals();
				exportKeywords();
			}

			// initialize buffer
			buf = new StringBuffer();

			// prepare HTML code
			try {
				iSyntax.export(this);
			} catch (NotSupportedExportKindException e) {
				sLog.log(Level.SEVERE, e.getMessage(), e);
			}

			// write HTML file
			writeFile(subDir, filename);
		}
	}

	public void exportReferences(String key, Set<String>	references) {

		if (key != null && references != null) {

			// initialize exporter
			buf = new StringBuffer();
			kind = ExportKind.HTML;

			// prepare HTML code
			appendHtmlFile(key, genLink(formatBold(key), HTML_PARENT_DIR , HTML_FILE_SYNTAX, key, false), HTML_START_TAG);
			appendLineSeparator();
			buf.append("Used in:");
			for (String reference : references) {
				appendLineBreak();
				appendLink(reference, HTML_PARENT_DIR , HTML_FILE_SYNTAX, reference);
			}
			appendHtmlFile(null, HTML_END_TAG);

			// write HTML file
			writeFile(this.htmlRoot+HTML_SUB_DIR_NONTERMINALS,key);
		}
	}

	public Exporter append(String str) {
		buf.append(str);
		return this;
	}

	public Exporter append(int integer) {
		buf.append(integer);
		return this;
	}


	public String formatStr(String str, boolean bold, boolean italic, String colorName, boolean append) {
		boolean colored = (colorName != null && !colorName.isEmpty());
		StringBuffer formatBuf;

		if (append) {
			// append formated string directly to the global StringBuffer
			formatBuf = buf;
		} else {
			// return the formated string, use local StringBuffer
			formatBuf = new StringBuffer();
		}

		if (!HTML_END_TAG.equals(str)) {
			// do not append start tag, when only end tag is needed
			if (bold)		formatBuf.append("<b>");
			if (italic)		formatBuf.append("<i>");
			if (colored)	formatBuf.append("<font color=").append(colorName).append(">");
		}

		if (!HTML_START_TAG.equals(str) && !HTML_END_TAG.equals(str)) {
			// do not append string, when only either start or end tag is needed
			formatBuf.append(str);
		}

		if (!HTML_START_TAG.equals(str)) {
			// do not append end tag, when only start tag is needed
			if (colored)	formatBuf.append("</font>");
			if (italic)		formatBuf.append("</i>");
			if (bold)		formatBuf.append("</b>");
		}

		if (append) {
			return null;
		} else {
			return formatBuf.toString();
		}
	}

	public String formatBold(String str) {
		return formatStr(str, true, false, null, false);
	}

	public String formatItalic(String str) {
		return formatStr(str, false, true, null, false);
	}

	public String formatColored(String str, String colorName) {
		return formatStr(str, false, false, colorName, false);
	}

	public Exporter appendBold(String str) {
		formatStr(str, true, false, null, true);
		return this;
	}

	public Exporter appendItalic(String str) {
		formatStr(str, false, true, null, true);
		return this;
	}

	public Exporter appendColored(String str, String colorName) {
		formatStr(str, false, false, colorName, true);
		return this;
	}

	public void formatKeyword(String str, boolean append) {
		formatStr(str, true, false, "green", append);
	}

	public void appendHeading(int which, String str) {
		if (which >= 1 && which <= 6) {
			buf.append("<h").append(which).append(">");
			buf.append(str);
			buf.append("</h").append(which).append(">");
		}
	}

	public void appendLineBreak() {
		buf.append("<br/>");
	}

	public void appendLineSeparator() {
		buf.append("<hr/>");
	}

	public void appendWithAnchor(String str, String anchor) {
		buf.append("<a name=\"").append(anchor).append("\">").append(str).append("</a>");
	}

	public String genLink(String str, String relDir, String file, String anchor, boolean append) {
		StringBuffer linkBuf;

		if (append) {
			// append generated link directly to the global StringBuffer
			linkBuf = buf;
		} else {
			// return the generated link, use local StringBuffer
			linkBuf = new StringBuffer();
		}

		linkBuf.append("<a href=\"");

		// link to different file (and directory)
		if (file != null && !file.isEmpty()) {
			if (relDir != null && !relDir.isEmpty()) {
				linkBuf.append(relDir);
			}
			linkBuf.append(file).append(HTML_FILEENDING);
		}

		// link to specific anchor within file
		if (anchor != null && !anchor.isEmpty()) {
			linkBuf.append("#").append(anchor);
		}

		linkBuf.append("\">").append(str).append("</a>");


		if (append) {
			return null;
		} else {
			return linkBuf.toString();
		}
	}

	public void appendLink(String str, String relDir, String file, String anchor) {
		genLink(str, relDir, file, anchor, true);
	}

	public void appendLink(String str, String anchor) {
		appendLink(str, null, null, anchor);
	}

	public void appendLink(String str, String relDir, String file) {
		appendLink(str, relDir, file, null);
	}


	public void appendNonterminal(String str) {

		switch (kind) {
		case TXT:
			buf.append(String.format(FORMAT_NONTERMINAL, str));
			break;
		case HTML:
			appendWithAnchor(formatBold(str),str);
			break;
		case HTMLCOMPACT:
			appendBold(str);
			break;
		}
	}

	public void appendNonterminalReferences(String nonterminal) {

		Set<String>	references = nonterminals.get(nonterminal);
		if (references != null) {
			int i,max_ref=5;

			appendColored(HTML_START_TAG,"grey");
			buf.append(" (used in: ");

			Iterator<String> iterator = references.iterator();
			for (i=0;iterator.hasNext();i++) {
				String reference = iterator.next();
				if (i<max_ref) {
					if (i>0) {
						buf.append(", ");
					}
					appendLink(reference,reference);
				}
			}
			if (i>max_ref) {
				buf.append(" and ");
				buf.append(i-max_ref);
				buf.append(" ");
				appendLink("more", HTML_SUB_DIR_NONTERMINALS, nonterminal);

				// create an own HTML page just for the references
				Exporter refExporter = new Exporter(this);
				refExporter.exportReferences(nonterminal,references);
			}
			buf.append(")");
			appendColored(HTML_END_TAG,"grey");
		}
	}

	public void appendHtmlFile(String title, String heading, String content) {

		if (!HTML_END_TAG.equals(content) && title != null && heading != null) {
			// do not append start tag, when only end tag is needed
			Date currentTimestamp = new java.sql.Timestamp(Calendar.getInstance().getTime().getTime());

			buf.append("");
			buf.append("<html>");
			buf.append("<head><title>").append(title).append("</title></head>");
			buf.append("<body>");
			if (kind == ExportKind.HTML) {
				// do not add this in compact mode:
				appendHeading(1,heading);
				appendBold("Generated at: ").append(currentTimestamp.toString()).appendLineBreak();
			}
		}

		if (!HTML_START_TAG.equals(content) && !HTML_END_TAG.equals(content)) {
			// do not append string, when only either start or end tag is needed
			buf.append(content);
		}

		if (!HTML_START_TAG.equals(content)) {
			// do not append end tag, when only start tag is needed
			buf.append("</body>");
			buf.append("</html>");
		}
	}

	public void appendHtmlFile(String title, String content) {
		appendHtmlFile(title, title, content);
	}

	/*
	 * REFERENCES
	 */
	private void fillReferenceMaps(Syntax syntax) {
		keywords = new ReferenceMap();
		nonterminals = new ReferenceMap();

		syntax.fillReferenceMap(keywords,FactorType.KEYWORD);
		syntax.fillReferenceMap(nonterminals,FactorType.NONTERMINAL);
	}

	private void writeFile(String subDir, String filename) {

		// make sure the output directory exists
		File path = new File(this.outputDirectory+File.separator+subDir);
		path.mkdirs();

		// determine filename
		File file = new File(path.getAbsolutePath()+File.separator+filename+HTML_FILEENDING);

		try {
			// create file
			BufferedWriter out = new BufferedWriter(
			                     new FileWriter(file));
			out.write(buf.toString());
			out.close();
		}
		catch (IOException e) {
			sLog.log(Level.SEVERE,"Error on writing output file ["+filename+"] in directory ["+path.getAbsolutePath()+"]: "+e.getMessage(),e);
		}
	}

	private void loadProps() {
		// load properties
		props = new Properties();
		try {
			BufferedInputStream stream = new BufferedInputStream(new FileInputStream("./res/export.properties"));
			props.load(stream);
			stream.close();
		}
		catch (IOException e) {
			sLog.log(Level.SEVERE,"Error on reading file [export.properties] in directory [./res]: "+e.getMessage(),e);
		}

		handleProps();
	}

	private void handleProps() {
		int idx=1;

		// prepare content list
		content = new ArrayList<Chapter>();
		while (props.containsKey(PROP_CHAPTER+idx)) {
			String nonterminal = props.getProperty(PROP_CHAPTER+idx);
			String title = props.getProperty(PROP_CHAPTER+idx+PROP_SUFFIX_TITLE);

			content.add(new Chapter(nonterminal,title));

			idx++;
		}

		// prepare extracts
		extracts = new HashSet<String>();
		String extractStr = props.getProperty(PROP_EXTRACT);
		if (extractStr != null) {
			extracts.addAll(Arrays.asList(extractStr.split(",")));
		}
	}

	private String buildChapterDir(Chapter chapter) {
		return (HTML_DIR_PREFIX_CHAPTER+"__"+chapter.getNontermial()+"/");
	}

	/*
	 * EXPORT
	 */
	private void exportTableOfContent() {
		int idx=1;

		// initialize buffer
		buf = new StringBuffer();

		// determine title
		String title = "Syntax Reference";
		if (props.containsKey(PROP_TITLE)) {
			title = props.getProperty(PROP_TITLE);
		}

		appendHtmlFile(title, Exporter.HTML_START_TAG);
		appendLineSeparator();
		appendHeading(2, "1) "+genLink("Complete Syntax", HTML_SUB_DIR_SYNTAX, HTML_FILE_SYNTAX, null, false));
		appendHeading(2, "2) "+genLink("Nonterminals", "", HTML_FILE_NONTERMINALS, null, false));
		appendHeading(2, "3) "+genLink("Keywords", "", HTML_FILE_KEYWORDS, null, false));
		if (!content.isEmpty()) {
			appendHeading(2, "4) Chapters:");
		}
		for (Chapter chapter : content) {
			String chapterStr = "4."+Integer.toString(idx++)+") ";
			// generate link to chapter and add it to the string
			chapterStr += genLink(chapter.getTitle(), buildChapterDir(chapter), HTML_FILE_SYNTAX, null, false);

			appendHeading(3, chapterStr);
		}
		appendHtmlFile(null, Exporter.HTML_END_TAG);

		// write HTML file
		writeFile("", HTML_FILE_INDEX);
	}

	private void exportChapters(Syntax completeSyntax) {

		for (Chapter chapter : content) {
			Syntax chapterSyntax = completeSyntax.extractSyntax(chapter.getTitle(),chapter.getNontermial());

			exportSyntax(chapterSyntax, buildChapterDir(chapter), HTML_FILE_SYNTAX, false);
		}
	}

	private void exportNonterminals() {
		// initialize buffer
		buf = new StringBuffer();

		// prepare HTML content
		appendHtmlFile("Nonterminals", HTML_START_TAG);
		appendBold("Number of nonterminals: ").append(nonterminals.size());
		appendLineSeparator();
		for (String key : nonterminals.keySet()) {
			appendLink(key, HTML_SUB_DIR_SYNTAX , HTML_FILE_SYNTAX, key);
			appendLineBreak();
		}
		appendHtmlFile(null, HTML_END_TAG);

		// write HTML file
		writeFile("", HTML_FILE_NONTERMINALS);
	}


	private void exportKeywords() {
		// initialize buffer
		buf = new StringBuffer();

		// prepare HTML content
		appendHtmlFile("Keywords", HTML_START_TAG);
		appendBold("Number of keywords: ").append(keywords.size());
		appendLineSeparator();
		for (String key : keywords.keySet()) {
			formatKeyword(key,true);
			appendLineBreak();
		}
		appendHtmlFile(null, HTML_END_TAG);

		// write HTML file
		writeFile("", HTML_FILE_KEYWORDS);
	}

	/*
	 * Extracts are in contrast to chapters exported as compact HTML and are not part of the TableOfContent.
	 */
	private void exportExtracts(Syntax completeSyntax) {

		// switch temporary to compact HTML export
		ExportKind prevKind = kind;
		this.kind = ExportKind.HTMLCOMPACT;

		for (String nonterminal : extracts) {
			Syntax extractedSyntax = completeSyntax.extractSyntax(nonterminal,nonterminal);

			exportSyntax(extractedSyntax, HTML_SUB_DIR_EXTRACTED, nonterminal, false);
		}

		this.kind = prevKind;
	}
}
